package service

import (
	"context"
	"strings"

	// "time"

	"database/sql"

	dbmodel "gitee.com/hp-huiw/family-expense-api/internal/db/model"
	"gitee.com/hp-huiw/family-expense-api/internal/dto"
	"gitee.com/hp-huiw/family-expense-api/internal/repository"
	"gitee.com/hp-huiw/my-go-core/date"
	"gitee.com/hp-huiw/my-go-core/util"
	"github.com/samber/lo"
)

type IncomeExpenseDetailTypeService interface {
	InitializeWithTx(ctx context.Context, tx *sql.Tx, accountGroupID string) error
	Query(ctx context.Context, accountGroupID string, incomeExpenseType string) ([]dto.IncomeExpenseDetailTypeResponse, error)
	QueryNameForAnalysis(ctx context.Context, accountGroupID string, incomeExpenseType string) ([]string, error)
	QueryAccountGroupDetailTypes(ctx context.Context, incomeExpenseType string, names []string, accountGroupID string) ([]string, error)
	ExistsBy(ctx context.Context, accountGroupID string, incomeExpenseType string, id string, name string) (bool, error)
	QueryMaxOrderIndex(ctx context.Context, accountGroupID string, incomeExpenseType string) (int, error)
	QueryNextMaxOrderIndex(ctx context.Context, accountGroupID string, incomeExpenseType string) (int, error)
	QueryAnalysisNotIncludedDetailTypes(ctx context.Context, accountGroupID string, incomeExpenseType string) ([]dto.IncomeExpenseDetailTypeBasicInfoResponse, error)
	ResetOrder(ctx context.Context, accountGroupID string, detailTypeIDs []string) error
	Add(ctx context.Context, data *dto.IncomeExpenseDetailTypeRequest, accountGroupID string) (*dto.IncomeExpenseDetailTypeResponse, error)
	Update(ctx context.Context, data *dto.IncomeExpenseDetailTypeRequest, accountGroupID string) (*dto.IncomeExpenseDetailTypeResponse, error)
	Delete(ctx context.Context, incomeExpenseType string, name string, accountGroupID string) (bool, error)
	BatchDelete(ctx context.Context, incomeExpenseType string, names []string, accountGroupID string) ([]string, error)
	DeleteWithTx(ctx context.Context, tx *sql.Tx, accountGroupID string) error
}

func NewIncomeExpenseDetailTypeService(db *sql.DB) IncomeExpenseDetailTypeService {
	return &incomeExpenseDetailTypeService{
		db:                                db,
		incomeExpenseDetailTypeRepository: repository.NewIncomeExpenseDetailTypeRepository(db),
		incomeExpenseDetailService:        NewIncomeExpenseDetailService(db),
	}
}

func (s *incomeExpenseDetailTypeService) InitializeWithTx(ctx context.Context, tx *sql.Tx, accountGroupID string) error {
	exists, err := s.incomeExpenseDetailTypeRepository.Exists(ctx, accountGroupID)

	if err != nil {
		return err
	}

	// Prepares default detail types if needed.
	if !exists {
		// Queries all pre-defined base detail types.
		baseDetailTypes, _ := NewIncomeExpenseDetailBaseTypeService(s.db).QueryAll(ctx)

		// Creates detail types based on base detail types.
		detailTypes := lo.Map(baseDetailTypes, func(item dbmodel.IncomeExpenseDetailBaseType, _ int) dbmodel.IncomeExpenseDetailType {
			return dbmodel.IncomeExpenseDetailType{
				ID:                util.GenerateUUIDWithoutDashes(),
				AccountGroupID:    accountGroupID,
				Name:              item.Name,
				Title:             item.Title,
				Subtitle:          item.Subtitle,
				OrderIndex:        item.OrderIndex,
				IncomeExpenseType: item.IncomeExpenseType,
				AnalysisIncluded:  item.AnalysisIncluded,
				DateCreated:       date.GetCurrentDate(),
				LastUpdated:       date.GetCurrentDate(),
			}
		})

		// Starts batch insert.
		return s.incomeExpenseDetailTypeRepository.BatchInsertWithTx(ctx, tx, detailTypes)
	}

	return nil
}

// Queries all detail types which associated with provided account group.
func (s *incomeExpenseDetailTypeService) Query(ctx context.Context, accountGroupID string, incomeExpenseType string) ([]dto.IncomeExpenseDetailTypeResponse, error) {
	detailTypes, err := s.incomeExpenseDetailTypeRepository.Query(ctx, accountGroupID, incomeExpenseType)

	if err != nil {
		return nil, err
	}

	result := lo.Map(detailTypes, func(item repository.IncomeExpenseDetailType, _ int) dto.IncomeExpenseDetailTypeResponse {
		return dto.IncomeExpenseDetailTypeResponse{
			IncomeExpenseDetailType: dto.IncomeExpenseDetailType{
				ID: &item.ID,
				IncomeExpenseDetailTypeBasic: dto.IncomeExpenseDetailTypeBasic{
					Name:  item.Name,
					Title: item.Title,
				},
				IncomeExpenseType: item.IncomeExpenseType,
				Subtitle:          item.Subtitle,
				AnalysisIncluded:  item.AnalysisIncluded,
			},
			Timestamps: dto.Timestamps{
				DateCreated: &item.DateCreated,
				LastUpdated: &item.LastUpdated,
			},
		}
	})

	return result, nil
}

// Queries all detail types which associated with provided account group.
// return List of name string.
func (s *incomeExpenseDetailTypeService) QueryNameForAnalysis(ctx context.Context, accountGroupID string, incomeExpenseType string) ([]string, error) {
	return s.incomeExpenseDetailTypeRepository.QueryNames(ctx, accountGroupID, incomeExpenseType)
}

// Queries the input name list and makes sure they belong to the specific account group.
// return The matched name list.
func (s *incomeExpenseDetailTypeService) QueryAccountGroupDetailTypes(ctx context.Context, incomeExpenseType string, names []string, accountGroupID string) ([]string, error) {
	return s.incomeExpenseDetailTypeRepository.QueryAccountGroupDetailTypes(ctx, incomeExpenseType, names, accountGroupID)
}

// Checks if the detail type name is already existing with the same account id, income/expense type and id.
// return True means the input detail type name already exists. False means NOT.
func (s *incomeExpenseDetailTypeService) ExistsBy(ctx context.Context, accountGroupID string, incomeExpenseType string, id string, name string) (bool, error) {
	name = strings.ToUpper(name)
	return s.incomeExpenseDetailTypeRepository.ExistsByOnDiffID(ctx, accountGroupID, &incomeExpenseType, &id, &name)
}

// Queries the current max order index with provided account group id and income/expense type.
// return The int number of current max order index.
func (s *incomeExpenseDetailTypeService) QueryMaxOrderIndex(ctx context.Context, accountGroupID string, incomeExpenseType string) (int, error) {
	return s.incomeExpenseDetailTypeRepository.QueryMaxOrderIndex(ctx, accountGroupID, incomeExpenseType)
}

// Queries the next max order index with provided account group id and income/expense type.
// return The int number of next max order index.
func (s *incomeExpenseDetailTypeService) QueryNextMaxOrderIndex(ctx context.Context, accountGroupID string, incomeExpenseType string) (int, error) {
	maxOrderIndex, err := s.QueryMaxOrderIndex(ctx, accountGroupID, incomeExpenseType)

	if err != nil {
		return -1, err
	}

	return maxOrderIndex + 1, nil
}

// Queries all detail types which associated with provided account group which is not included by analysis.
func (s *incomeExpenseDetailTypeService) QueryAnalysisNotIncludedDetailTypes(ctx context.Context, accountGroupID string, incomeExpenseType string) ([]dto.IncomeExpenseDetailTypeBasicInfoResponse, error) {
	query, err := s.incomeExpenseDetailTypeRepository.QueryAnalysisNotIncludedDetailTypes(ctx, accountGroupID, incomeExpenseType)

	if err != nil {
		return nil, err
	}

	result := lo.Map(query, func(item repository.IncomeExpenseDetailTypeBasicInfo, _ int) dto.IncomeExpenseDetailTypeBasicInfoResponse {
		return dto.IncomeExpenseDetailTypeBasicInfoResponse{
			IncomeExpenseDetailTypeBasic: dto.IncomeExpenseDetailTypeBasic{
				Name:  item.Name,
				Title: item.Title,
			},
		}
	})

	return result, nil
}

// Resets the detail types order based on the sequence of provided id list.
func (s *incomeExpenseDetailTypeService) ResetOrder(ctx context.Context, accountGroupID string, detailTypeIDs []string) error {
	return s.incomeExpenseDetailTypeRepository.ResetOrder(ctx, accountGroupID, detailTypeIDs)
}

// Adds new detail type for account group.
func (s *incomeExpenseDetailTypeService) Add(ctx context.Context, data *dto.IncomeExpenseDetailTypeRequest, accountGroupID string) (*dto.IncomeExpenseDetailTypeResponse, error) {
	exists, err := s.ExistsBy(ctx, accountGroupID, data.IncomeExpenseType, "", data.Name)

	if err != nil {
		return nil, err
	}

	if !exists {
		// Gets data from dto data.
		bankCard := dbmodel.IncomeExpenseDetailType{
			ID:                util.GenerateUUIDWithoutDashes(),
			AccountGroupID:    accountGroupID,
			IncomeExpenseType: data.IncomeExpenseType,
			Name:              strings.ToUpper(data.Name),
			Title:             data.Title,
			Subtitle:          data.Subtitle,
			AnalysisIncluded:  *data.AnalysisIncluded,
			DateCreated:       date.GetCurrentDate(),
			LastUpdated:       date.GetCurrentDate(),
		}

		orderIndex, err := s.QueryNextMaxOrderIndex(ctx, accountGroupID, bankCard.IncomeExpenseType)

		if err != nil {
			return nil, err
		}

		bankCard.OrderIndex = int32(orderIndex)

		if err := s.incomeExpenseDetailTypeRepository.Insert(ctx, &bankCard); err != nil {
			return nil, err
		}

		savedDetailType := dto.IncomeExpenseDetailTypeResponse{
			IncomeExpenseDetailType: dto.IncomeExpenseDetailType{
				ID: &bankCard.ID,
				IncomeExpenseDetailTypeBasic: dto.IncomeExpenseDetailTypeBasic{
					Name:  bankCard.Name,
					Title: bankCard.Title,
				},
				IncomeExpenseType: bankCard.IncomeExpenseType,
				Subtitle:          bankCard.Subtitle,
				AnalysisIncluded:  &bankCard.AnalysisIncluded,
			},
		}

		return &savedDetailType, nil
	}

	return nil, nil
}

// Updates detail type based on provided data and its id.
func (s *incomeExpenseDetailTypeService) Update(ctx context.Context, data *dto.IncomeExpenseDetailTypeRequest, accountGroupID string) (*dto.IncomeExpenseDetailTypeResponse, error) {
	// Checks if the updated name with type if already exists with different id.
	newNameExists, err := s.incomeExpenseDetailTypeRepository.ExistsByOnDiffID(ctx, accountGroupID, &data.IncomeExpenseType, data.ID, &data.Name)

	if err != nil {
		return nil, err
	}

	// Retrieves current detail type.
	detailType, err := s.incomeExpenseDetailTypeRepository.Get(ctx, accountGroupID, *data.ID)

	if err != nil {
		return nil, err
	}

	if !newNameExists && detailType != nil {
		// Gets current detail type name for income expense detail reference update.
		currentName := detailType.Name

		// Updates fields based on provided data.
		detailType.Name = strings.ToUpper(data.Name)
		detailType.Title = data.Title
		detailType.Subtitle = data.Subtitle
		detailType.AnalysisIncluded = *data.AnalysisIncluded
		detailType.LastUpdated = date.GetCurrentDate()

		// To perform a set of operations within a transaction.
		tx, err := s.db.Begin()

		if err != nil {
			return nil, err
		}

		defer tx.Rollback()

		// Updates detail type.
		if err = s.incomeExpenseDetailTypeRepository.UpdateWithTx(ctx, tx, detailType); err != nil {
			return nil, err
		}

		// Updates income expense detail refernece.
		if err = s.incomeExpenseDetailService.UpdateDetailTypeWithTx(ctx, tx, accountGroupID, detailType.IncomeExpenseType, currentName, detailType.Name); err != nil {
			return nil, err
		}

		if err = tx.Commit(); err != nil {
			return nil, err
		}

		return &dto.IncomeExpenseDetailTypeResponse{
			IncomeExpenseDetailType: dto.IncomeExpenseDetailType{
				ID: &detailType.ID,
				IncomeExpenseDetailTypeBasic: dto.IncomeExpenseDetailTypeBasic{
					Name:  detailType.Name,
					Title: detailType.Title,
				},
				IncomeExpenseType: detailType.IncomeExpenseType,
				Subtitle:          detailType.Subtitle,
				AnalysisIncluded:  &detailType.AnalysisIncluded,
			},
			Timestamps: dto.Timestamps{
				DateCreated: &detailType.DateCreated,
				LastUpdated: &detailType.LastUpdated,
			},
		}, nil
	}

	return nil, nil
}

// Deletes the IncomeExpenseDetailTypes with provides list of id.
// return True means deleted. False means this detail type has been used and cannot be deleted.
func (s *incomeExpenseDetailTypeService) Delete(ctx context.Context, incomeExpenseType string, name string, accountGroupID string) (bool, error) {
	// Checks if this detail type is used by income/expense.
	exists, err := s.incomeExpenseDetailService.DetailTypeExists(ctx, accountGroupID, incomeExpenseType, name)

	if err != nil {
		return false, err
	}

	if !exists {
		// If this detail type is not used then deletes it.
		if err = s.incomeExpenseDetailTypeRepository.DeleteByName(ctx, accountGroupID, incomeExpenseType, name); err != nil {
			return false, err
		}

		return true, nil
	}

	return false, nil
}

// Deletes the IncomeExpenseDetailTypes with provides list of name.
// return The list of the id string which have been actually deleted.
func (s *incomeExpenseDetailTypeService) BatchDelete(ctx context.Context, incomeExpenseType string, names []string, accountGroupID string) ([]string, error) {
	var deletedDetailTypeIDs []string

	// Queries the input id list and makes sure they belong to the specific account group.
	// This is an example shows how to secure the data by taking extra action.
	// This can void other account group users call api directly and provide these ids which they don't actually own them.
	names, err := s.QueryAccountGroupDetailTypes(ctx, incomeExpenseType, names, accountGroupID)

	if err != nil {
		return nil, err
	}

	// Queries all detail type names which used by income/expense.
	usedNames, err := s.incomeExpenseDetailService.QueryUsedDetailTypeNames(ctx, incomeExpenseType, names, accountGroupID)

	if err != nil {
		return nil, err
	}

	// Excludes all used detail type names.
	unusedNames := lo.Without(names, usedNames...)

	if len(unusedNames) > 0 {
		// region: DELETE with RETURNING
		// https://github.com/go-jet/jet/wiki/DELETE
		// To execute PostgreSQL delete statement and return records deleted, delete statement has to have RETURNING clause:
		// Otherwise you have to make another query to get deleted records before deleting.
		//
		// deleteStmt := Link.
		// DELETE().
		// WHERE(Link.Name.IN(String("Gmail"), String("Outlook"))).
		// RETURNING(Link.AllColumns)
		//
		// dest := []model.Link{}
		// err := deleteStmt.Query(db, &dest)
		// endregion

		// Queries all ids based on incomeExpenseType and names.
		// This is will be returned to client for list item managing.
		if deletedDetailTypeIDs, err = s.incomeExpenseDetailTypeRepository.QueryIDs(ctx, accountGroupID, incomeExpenseType, unusedNames); err != nil {
			return nil, err
		}

		// Executes batch deleting to delete any unused detail type.
		if err = s.incomeExpenseDetailTypeRepository.DeleteByIDs(ctx, deletedDetailTypeIDs); err != nil {
			return nil, err
		}
	}

	return deletedDetailTypeIDs, nil
}

// DeleteWithTx deletes all income and expense detail types by account group id with transaction.
func (s *incomeExpenseDetailTypeService) DeleteWithTx(ctx context.Context, tx *sql.Tx, accountGroupID string) error {
	return s.incomeExpenseDetailTypeRepository.DeleteWithTx(ctx, tx, accountGroupID)
}

// Container of IncomeExpenseDetailTypeService functions.
type incomeExpenseDetailTypeService struct {
	db                                *sql.DB
	incomeExpenseDetailTypeRepository repository.IncomeExpenseDetailTypeRepository
	incomeExpenseDetailService        IncomeExpenseDetailService
}
